Aller au contenu

Comment utiliser le Test-Driven Development sur Salesforce pour vos intégrations API ?

Avis d'experts

04 février 2026

Les Tests Unitaires sont en général vus par les développeurs Salesforce comme une contrainte et une corvée. Trop souvent, ils sont relégués à la fin du projet, alors qu’ils peuvent aider à écrire du code solide. Pire parfois, lorsqu’ils n’ont pas été anticipés, ils deviennent problématiques à écrire.

 

Pour l’intégration avec des API externes, ils peuvent faciliter le développement, en permettant par exemple de les écrire même lorsque l’on n’a pas encore accès à l’API, pour peu que l’on connaisse la structure des données que l’on récupérera.

 

Dans notre exemple, nous supposerons que nous avons une API Magellan externe.

 

Le principe sous-jacent est d’utiliser les classes de tests pour valider directement le développement des classes métiers.

On utilisera les Static Resources pour simuler exactement le retour de l’API. L’utilisation des Static Resources est d’une part plus simple que de mettre le contenu dans une simple string. Elle permet d’identifier facilement tous les cas de figure que l’on peut rencontrer avec l’API. Elle rend plus simple le déploiement. Pour tester une évolution de l’API, il suffit de mettre à jour la ressource ou d’en ajouter une nouvelle.

Étape 1 : la classe technique Magellan APIConnector

public with sharing class Magellan APIConnector {

    /**
     * Un exemple d'appel API REST vers un service externe.
     * Le service retourne des données au format JSON.
     */
    public static Map<String, Object> callMagellan API(String Magellan Id) {
        Map<String, Object> result = new Map<String, Object>();

        String endpoint = 'https://api.Magellan .com/v1/Magellan Id/' + Magellan Id;

        HttpRequest request = new HttpRequest();
        request.setEndpoint(endpoint);
        request.setMethod('GET');
        request.setHeader('Accept', 'application/json');

        Http http = new Http();
        HttpResponse response;
        try {
            response = http.send(request);
            if (response.getStatusCode() == 200) {
                result.put('status', 'found');
                Map<String, Object> responseMap = (Map<String, Object>) JSON.deserializeUntyped(response.getBody());
                result.put('data', responseMap);
                return result;
            } else if (response.getStatusCode() == 404) {
                result.put('status', 'not_found');
                return result;
            } else {
                result.put('status', 'error');
                result.put('error', 'Error while calling API ' + response.getStatusCode() + ' - ' + response.getBody());
                return result;
            }
        } catch (Exception exc) {
            result.put('status', 'error');
            result.put('error', 'Error while calling API ' + 		response.getStatusCode() + ' - ' + response.getBody());
            result.put('error', 'Error while calling API ' + exc.getMessage());
            return result;
        }
    }

}

Étape 2 : créer une classe implémentant MockHttpResponseGenerator

/**
 * Cette classe est une implémentation fictive de l'interface HttpCalloutMock.
 * Elle est utilisée pour simuler des réponses HTTP lors des tests.
**/
@isTest
global with sharing class MockHttpResponseGenerator implements HttpCalloutMock{

    public Integer statusCode;
    public String body = '';
    public String contentType = 'application/json';
    public String status = '';
     
    /**
     * Constructeur pour générer une réponse HTTP, lorsque nous ne testons que le status code    
     */ 
    global MockHttpResponseGenerator(Integer statusCode) {
        this.statusCode = statusCode;
    }

    /**
     * Constructeur pour générer une réponse HTTP complète, avec le status code et le body    
     */
    global MockHttpResponseGenerator(Integer statusCode, String body) {
        this.statusCode = statusCode;
        this.body = body;
    }

    /**
     * Implémentation de la méthode respond de l'interface HttpCalloutMock
     */
    global HTTPResponse respond(HTTPRequest req) {

        HttpResponse response = new HttpResponse();
        
        response.setHeader('Content-Type', this.contentType);
        response.setBody(this.body);
        response.setStatusCode(this.statusCode);
        response.setStatus(this.status);

        return response;
    }

}

Étape 3 : la classe de test Magellan APIConnector_Test

/**
 * Le but de la classe est de fournir d'une part la couverture de code maximale
 * et d'autre part de tester les différents scénarios possibles lors d'un appel API
 * avec les données utilisées
 */
@isTest
public with sharing class Magellan APIConnector_Test {
    
    /**
     * Test d'un appel API réussi (status 200) avec des données retournées
     */
    @isTest
    static void test_callMagellan APIFound() {

        /**
         * Nous utilisons une ressource statique pour stocker la réponse JSON de l'API.
         * Cela nous permet de simuler une réponse réaliste sans dépendre d'une API externe
         */
        StaticResource staticResource = [SELECT Body FROM StaticResource WHERE Name = 'test_api_Magellan ' LIMIT 1];
        String jsonResponse = staticResource.Body.toString();

        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator(200, jsonResponse));

        Test.startTest();
        Map<String, Object> result = Magellan APIConnector.callMagellan API('1234567890');
        Test.stopTest();
        
        System.assertEquals('found', result.get('status'));
        System.assertNotEquals(null, result.get('data'));

        /**
         * Nous vérifions que la réponse contient bien les données attendues
         * souvent il s'agira de données plus complexes qu'une simple Map
         * et l'on pourra utiliser les règles métier pour valider les données
         * reçues de l'API
         */
        Map<String, Object> data = (Map<String, Object>) result.get('data');
        System.assertNotEquals(null, data.get('myImportantData'));

        Map<String, Object> myImportantData = (Map<String, Object>) data.get('myImportantData');
        System.assertEquals('area51', myImportantData.get('importantField'));

    }

    /**
     * Test d'un appel API avec un status 404 (not found)
     * l'appel est réussi mais l'API ne trouve pas de données
     */
    @isTest
    static void test_callMagellan APINotFound() {

        String bodyResponse = '{"statut":404,"message":"No data found for the given Magellan Id"}';

        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator(404, bodyResponse));

        Test.startTest();
        Map<String, Object> result = Magellan APIConnector.callMagellan API('1234567890');
        Test.stopTest();
        
        System.assertEquals('not_found', result.get('status'));
        System.assertEquals(null, result.get('data'));

    }

    /**
     * Test d'un appel API avec un status 500 (internal server error)
     * le serveur de l'API a rencontré une erreur
     */
    @isTest
    static void test_callMagellan APIError500() {

        String bodyResponse = '{"statut":500,"message":"Internal Server Error"}';

        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator(500, bodyResponse));

        Test.startTest();
        Map<String, Object> result = Magellan APIConnector.callMagellan API('1234567890');
        Test.stopTest();
        
        System.assertEquals('error', result.get('status'));
        System.assertEquals(null, result.get('data'));

    }

    /**
     * Test d'un appel API qui génère une exception
     */
    @isTest
    static void test_callMagellan APIException() {

        /**
         * Pour simuler une exception nous utilisons un body qui n'est pas du JSON valide
         * mais l'exception peut aussi être générée par d'autres causes
         * comme un Remote Site non configuré, un timeout, etc.
         */
        String bodyResponse = 'json error';

        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator(200, bodyResponse));

        Test.startTest();
        Map<String, Object> result = Magellan APIConnector.callMagellan API('1234567890');
        Test.stopTest();
        
        System.assertEquals('error', result.get('status'));
        System.assertEquals(null, result.get('data'));

    }

}

Étape 4 : la classe consommant l’API

public class Magellan APIConsumer {
    
    /**
     * Exemple d'une méthode consommant l'API Magellan   .
     */
     public static void exampleCall(String Magellan Id) {
        Map<String, Object> result = Magellan APIConnector.callMagellan API(Magellan Id);

        if (result.get('status') == 'found') {
            // faire un traitement avec les données reçues
        } 
        if (result.get('status') == 'not_found') {
            // traiter une erreur fonctionnelle
        }
        if (result.get('status') == 'error') {
            // traiter une erreur technique
        }

    }
}

Étape 5 : la classe de test métier

@isTest
public class Magellan APIConsumer_Test {
    @isTest
    static void test_exampleCallUS1() {
        Test.startTest();
        Test.stopTest();

        StaticResource staticResource = [SELECT Body FROM StaticResource WHERE Name = 'test_api_Magellan _us1' LIMIT 1];
        String jsonResponse = staticResource.Body.toString();

        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator(200, jsonResponse));

        Test.startTest();
        Magellan APIConsumer.exampleCall('1234567890');
        Test.stopTest();

        // insérer ici des assertions pour vérifier les règles métier sont respectées 
        // pour la USER STORY 1
    }

    @isTest
    static void test_exampleCallUS2() {
        Test.startTest();
        Test.stopTest();

        StaticResource staticResource = [SELECT Body FROM StaticResource WHERE Name = 'test_api_Magellan _us2' LIMIT 1];
        String jsonResponse = staticResource.Body.toString();

        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator(200, jsonResponse));

        Test.startTest();
        Magellan APIConsumer.exampleCall('1234567890');
        Test.stopTest();

        // insérer ici des assertions pour vérifier les règles métier sont respectées
        // pour la USER STORY 2
    }

}

En résumé

Un décryptage de notre expert

PARTAGER L'ARTICLE

En savoir plus sur nos expertises

En savoir plus sur nos actualités

Envie d'aller plus loin avec nous